---
title: Vertex Layouts
description: A guide on how to create and use typed vertex layouts
---

:::note[Recommended reading]
We assume that you are familiar with the following concepts:
- <a href="https://webgpufundamentals.org/webgpu/lessons/webgpu-vertex-buffers.html" target="_blank" rel="noopener noreferrer">Vertex Buffers</a>
- <a href="https://www.w3.org/TR/webgpu/#vertex-state" target="_blank" rel="noopener noreferrer">WebGPU Vertex State</a>
- <a href="https://www.w3.org/TR/WGSL/#input-output-locations" target="_blank" rel="noopener noreferrer">Input-output locations</a>
- <a href="https://developer.mozilla.org/en-US/docs/Web/API/GPURenderPassEncoder" target="_blank" rel="noopener noreferrer">Render pass encoding</a>
:::

Vertex layouts are much like bind group layouts, in that they define the relationship between shaders and buffers. More precisely, they define what vertex attributes a shader expects, and how they are laid out in the corresponding vertex buffer.

## Creating a vertex layout

To create a vertex layout, use the `tgpu.vertexLayout` function. It takes an array schema constructor, i.e., partially applied `d.arrayOf` or `d.disarrayOf`. To determine what each element of the array corresponds to, you can pass an optional `stepMode` argument, which can be either `vertex` (default) or `instance`.

```ts
import tgpu from 'typegpu';
import * as d from 'typegpu/data';

const ParticleGeometry = d.struct({
  tilt: d.f32,
  angle: d.f32,
  color: d.vec4f,
});

const geometryLayout = tgpu
  .vertexLayout(d.arrayOf(ParticleGeometry), 'instance');
```

## Utilizing loose schemas with vertex layouts

If the vertex buffer is not required to function as a storage or uniform buffer, a *loose schema* may be used to define the vertex data layout. Loose schemas are not subject to alignment restrictions and allow the use of various [vertex formats](https://www.w3.org/TR/webgpu/#vertex-formats).

To define a loose schema:
- Use `d.unstruct` instead of `d.struct`.
- Use `d.disarrayOf` instead of `d.arrayOf`.

Within a loose schema, both standard data types and vertex formats can be utilized.

```ts
const LooseParticleGeometry = d.unstruct({
  tilt: d.f32,
  angle: d.f32,
  // four 8-bit values, unsigned & normalized
  // i.e., four integers in (0, 255) represent four floats in the range of (0.0, 1.0)
  color: d.unorm8x4,
});
```

The size of `LooseParticleGeometry` will be 12 bytes, compared to 32 bytes of `ParticleGeometry`. This can be useful when you're working with large amounts of vertex data and want to save memory.

:::tip[Aligning loose schemas]
Sometimes you might want to align the data in a loose schema due to external requirements or performance reasons.
You can do this by using the `d.align` function, just like in normal schemas. Even though loose schemas don't have alignment requirements, they will still respect any alignment you specify.

```ts
const LooseParticleGeometry = d.unstruct({
  tilt: d.f32,
  angle: d.f32,
  color: d.align(16, d.unorm8x4),
  // 4x8-bit unsigned normalized aligned to 16 bytes
});
```

This will align the `color` field to 16 bytes, making the size of `LooseParticleGeometry` 20 bytes.
:::

## Using vertex layouts

You can utilize [`root.unwrap`](/TypeGPU/fundamentals/roots) to get the raw `GPUVertexBufferLayout` from a typed vertex layout. It will automatically calculate the stride and attributes for you, according to the vertex layout you provided.

:::caution
Make sure that all attributes in the vertex layout are marked with the appropriate location. You can use the `d.location` function to specify the location of each attribute.
If you don't do this, the unwrapping will fail at runtime.
:::

```ts
const ParticleGeometry = d.struct({
  tilt: d.location(0, d.f32),
  angle: d.location(1, d.f32),
  color: d.location(2, d.vec4f),
});

const geometryLayout = tgpu
  .vertexLayout(d.arrayOf(ParticleGeometry), 'instance');

const geometry = root.unwrap(geometryLayout);

console.log(geometry);
//{
//  "arrayStride": 32,
//  "stepMode": "instance",
//  "attributes": [
//    {
//      "format": "float32",
//      "offset": 0,
//      "shaderLocation": 0
//    },
//    {
//      "format": "float32",
//      "offset": 4,
//      "shaderLocation": 1
//    },
//    {
//      "format": "float32x4",
//      "offset": 16,
//      "shaderLocation": 2
//    }
//  ]
//}
```

This will return a `GPUVertexBufferLayout` that can be used when creating a render pipeline.

```diff lang=ts
const renderPipeline = device.createRenderPipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [root.unwrap(bindGroupLayout)],
  }),
  primitive: {
    topology: 'triangle-strip',
  },
  vertex: {
    module: renderShader,
-    buffers: [
-      {
-        arrayStride: 32,
-        stepMode: 'instance',
-        attributes: [
-          {
-            format: 'float32',
-            offset: 0,
-            shaderLocation: 0,
-          },
-          {
-            format: 'float32',
-            offset: 4,
-            shaderLocation: 1,
-          },
-          {
-            format: 'float32x4',
-            offset: 16,
-            shaderLocation: 2,
-          },
-        ],
-      },
-    ],
+    buffers: [root.unwrap(geometryLayout)],
  },
  fragment: {
    ...
  },
});
```

Loose schemas can be interpreted in multiple ways within a shader. However, for convenience, they can be resolved to their default WGSL representation.

```ts
const LooseParticleGeometry = d.unstruct({
  tilt: d.location(0, d.f32),
  angle: d.location(1, d.f32),
  color: d.location(2, d.unorm8x4),
});

const sampleShader = `
  @vertex
  fn main(particleGeometry: LooseParticleGeometry) -> @builtin(position) pos: vec4f {
    return vec4f(
      particleGeometry.tilt,
      particleGeometry.angle,
      particleGeometry.color.rgb,
      1.0
    );
  }
`;

const wgslDefinition = tgpu.resolve({
  template: sampleShader,
  externals: { LooseParticleGeometry }
});

console.log(wgslDefinition);
// struct LooseParticleGeometry_0 {
//   @location(0) tilt: f32,
//   @location(1) angle: f32,
//   @location(2) color: vec4f,
// }
//
// @vertex
// fn main(particleGeometry: LooseParticleGeometry_0) -> @builtin(position) pos: vec4f {
//   return vec4f(
//     particleGeometry.tilt,
//     particleGeometry.angle,
//     particleGeometry.color.rgb,
//     1.0
//   );
// }
```
